pacenotes_installer\wizard\scene/
restore.rs1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
2use windows_sys::Win32::UI::Controls as ctl;
3use windows_sys::Win32::UI::WindowsAndMessaging as wm;
4
5pub unsafe extern "system" fn restore_page(
9 dialog: HWND,
10 message: u32,
11 wparam: WPARAM,
12 lparam: LPARAM,
13) -> isize {
14 match message {
15 wm::WM_INITDIALOG => {
16 crate::wizard::page::adopt_state(dialog, lparam);
17 crate::ui::set_text(
18 dialog,
19 crate::wizard::control_ids::IDC_RESTORE_PROMPT,
20 crate::i18n::IDS_RESTORE_PROMPT,
21 );
22 1
23 }
24 wm::WM_COMMAND => {
25 if crate::ui::hiword(wparam) == crate::ui::LBN_SELCHANGE
26 && crate::ui::loword(wparam) == crate::wizard::control_ids::IDC_RESTORE_POINTS
27 {
28 on_point_picked(dialog);
29 }
30 1
31 }
32 wm::WM_NOTIFY => match crate::wizard::page::notify_code(lparam) {
33 Some(ctl::PSN_SETACTIVE) => {
34 crate::wizard::page::set_sheet_caption(dialog);
35 crate::wizard::page::set_header(dialog, crate::i18n::IDS_RESTORE_TITLE);
36 crate::wizard::page::set_buttons(dialog, ctl::PSWIZB_BACK | ctl::PSWIZB_NEXT);
37 crate::wizard::page::set_next_text(dialog, crate::i18n::IDS_BTN_RESTORE);
38 fill_points(dialog);
39 crate::wizard::page::reply(dialog, 0)
40 }
41 Some(ctl::PSN_WIZBACK) => crate::wizard::page::reply(
43 dialog,
44 crate::wizard::control_ids::IDD_S3_FOLDER as isize,
45 ),
46 Some(ctl::PSN_WIZNEXT) => on_restore(dialog),
47 Some(ctl::PSN_QUERYCANCEL) => crate::wizard::page::query_cancel(dialog),
48 _ => 0,
49 },
50 _ => 0,
51 }
52}
53
54unsafe fn fill_points(dialog: HWND) {
57 let Some(state) = crate::wizard::page::state(dialog) else {
58 return;
59 };
60 let points = crate::wizard::control_ids::IDC_RESTORE_POINTS;
61 let preview = crate::wizard::control_ids::IDC_RESTORE_PREVIEW;
62 let game_dir = std::path::PathBuf::from(&state.folder);
63 state.points = pacenotes_installer::restore::points(&game_dir);
64 state.restore_to = None;
65 state.restore_plan = pacenotes_installer::restore::Plan::default();
66
67 crate::ui::list_reset(dialog, points);
68 crate::ui::set_text_raw(dialog, preview, "");
69 crate::ui::enable(dialog, points, !state.points.is_empty());
70 if state.points.is_empty() {
71 crate::ui::set_text(dialog, preview, crate::i18n::IDS_RESTORE_EMPTY);
72 return;
73 }
74 let live = pacenotes_installer::ledger::Ledger::open(&game_dir)
76 .packages()
77 .clone();
78 for (index, point) in state.points.iter().enumerate() {
79 let after = match index {
80 0 => &live,
81 _ => &state.points[index - 1].snapshot,
82 };
83 crate::ui::list_add(dialog, points, &point_label(point, after));
84 }
85 let everything = crate::i18n::load_string(crate::i18n::IDS_RESTORE_ALL);
86 crate::ui::list_add(dialog, points, &everything);
87}
88
89fn point_label(
91 point: &pacenotes_installer::restore::Point,
92 after: &pacenotes_installer::restore::Snapshot,
93) -> String {
94 let changes = pacenotes_installer::restore::changes(&point.snapshot, after);
95 let when = pacenotes_installer::restore::human_time(&point.name);
96 if changes.is_empty() {
97 return when;
98 }
99 format!(
100 "{when} +{} -{} ~{}",
101 changes.added, changes.removed, changes.updated
102 )
103}
104
105unsafe fn on_point_picked(dialog: HWND) {
108 let control = crate::wizard::control_ids::IDC_RESTORE_POINTS;
109 let Some(row) = crate::ui::list_selection(dialog, control) else {
110 return;
111 };
112 let Some(state) = crate::wizard::page::state(dialog) else {
113 return;
114 };
115 let point =
116 state
117 .points
118 .get(row)
119 .cloned()
120 .unwrap_or_else(|| pacenotes_installer::restore::Point {
121 name: crate::i18n::load_string(crate::i18n::IDS_RESTORE_ALL),
122 snapshot: pacenotes_installer::restore::Snapshot::new(),
123 });
124 let ledger = pacenotes_installer::ledger::Ledger::open(&state.folder);
125 let cache = &state.cache;
126 state.restore_plan =
127 pacenotes_installer::restore::plan(&point.snapshot, ledger.packages(), &|sha256| {
128 cache.has_payload(sha256)
129 });
130 state.restore_to = Some(point);
131 let text = describe_restore(&state.restore_plan);
132 crate::ui::set_text_raw(
133 dialog,
134 crate::wizard::control_ids::IDC_RESTORE_PREVIEW,
135 &text,
136 );
137}
138
139fn describe_restore(plan: &pacenotes_installer::restore::Plan) -> String {
142 if plan.is_empty() && plan.unavailable.is_empty() {
143 return crate::i18n::load_string(crate::i18n::IDS_RESTORE_NOTHING);
144 }
145 let put_back = |wanted: &pacenotes_installer::restore::PutBack| wanted.key.clone();
146 let sections = [
147 (crate::i18n::IDS_RESTORE_REMOVE, plan.remove.clone()),
148 (
149 crate::i18n::IDS_RESTORE_PUT_BACK,
150 plan.put_back.iter().map(put_back).collect(),
151 ),
152 (crate::i18n::IDS_RESTORE_KEEP, plan.keep.clone()),
153 (
154 crate::i18n::IDS_RESTORE_MISSING,
155 plan.unavailable.iter().map(put_back).collect(),
156 ),
157 ];
158 sections
159 .iter()
160 .filter(|(_, keys)| !keys.is_empty())
161 .map(|(heading, keys)| crate::i18n::load_string_arg(*heading, &leaves(keys)))
162 .collect::<Vec<_>>()
163 .join("\r\n\r\n")
164}
165
166fn leaves(keys: &[String]) -> String {
168 keys.iter()
169 .map(|key| format!(" {}", key.rsplit('/').next().unwrap_or(key)))
170 .collect::<Vec<_>>()
171 .join("\r\n")
172}
173
174unsafe fn on_restore(dialog: HWND) -> isize {
177 let Some(state) = crate::wizard::page::state(dialog) else {
178 return crate::wizard::page::reply(dialog, 0);
179 };
180 if state.restore_to.is_none() {
181 let pick = crate::i18n::load_string(crate::i18n::IDS_RESTORE_PICK);
182 crate::ui::message_box_warning(dialog, &pick);
183 return crate::wizard::page::stay(dialog);
184 }
185 if state.restore_plan.is_empty() {
187 let nothing = crate::i18n::load_string(crate::i18n::IDS_RESTORE_NOTHING);
188 crate::ui::message_box_info(dialog, ¬hing);
189 return crate::wizard::page::stay(dialog);
190 }
191 crate::wizard::page::reply(dialog, crate::wizard::control_ids::IDD_S6_INSTALL as isize)
192}